/*
 *   The contents of this file are subject to the Mozilla Public License
 *   Version 1.1 (the "License"); you may not use this file except in
 *   compliance with the License. You may obtain a copy of the License at
 *   http://www.mozilla.org/MPL/
 *
 *   Software distributed under the License is distributed on an "AS IS"
 *   basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 *   License for the specific language governing rights and limitations
 *   under the License.
 *
 *   The Original Code is Matra - the DTD Parser.
 *
 *   The Initial Developer of the Original Code is Conrad S Roche.
 *   Portions created by Conrad S Roche are Copyright (C) Conrad 
 *   S Roche. All Rights Reserved.
 *
 *   Alternatively, the contents of this file may be used under the terms
 *   of the GNU GENERAL PUBLIC LICENSE Version 2 or any later version
 *   (the  "[GPL] License"), in which case the
 *   provisions of GPL License are applicable instead of those
 *   above.  If you wish to allow use of your version of this file only
 *   under the terms of the GPL License and not to allow others to use
 *   your version of this file under the MPL, indicate your decision by
 *   deleting  the provisions above and replace  them with the notice and
 *   other provisions required by the GPL License.  If you do not delete
 *   the provisions above, a recipient may use your version of this file
 *   under either the MPL or the GPL License."
 *
 *   [NOTE: The text of this Exhibit A may differ slightly from the text of
 *   the notices in the Source Code files of the Original Code. You should
 *   use the text of this Exhibit A rather than the text found in the
 *   Original Code Source Code for Your Modifications.]
 *
 * Created: Conrad S Roche <derupe at users.sourceforge.net>,  27-Sep-2003
 */
package com.conradroche.matra.parser;

import com.conradroche.dtd.decl.Entity;
import com.conradroche.dtd.decl.GeneralEntity;
import com.conradroche.dtd.decl.ParameterEntity;
import com.conradroche.dtd.parser.EntityDeclReader;
import com.conradroche.matra.data.DTDData;
import com.conradroche.matra.exception.DTDSyntaxException;

/**
 * Class for reading entity declarations.
 *
 * @author Conrad Roche
 */
public class EntityDeclReaderImpl implements EntityDeclReader {

	/**
	 * Token identifying an entity declaration.
	 */
	private static final String ENTITY_DECL_START = "<!ENTITY";
	private static final String EXT_ID_PUBLIC = "PUBLIC";
	private static final String EXT_ID_SYSTEM = "SYSTEM";
	private static final String NDATA = "NDATA";
	
	/**
	 * EntityDeclReaderImpl Constructor.
	 */
	public EntityDeclReaderImpl() {
		super();
	}

	/**
	 * Read the entity declarations from 
	 * the data stream.
	 * 
	 * @param data The stream from which to 
	 * 			read the entity declaration.
	 * 
	 * @return The entity declaration.
	 * 
	 * @throws DTDSyntaxException If the Entity 
	 * 			declaration contains a syntax
	 * 			error. 
	 * 
	 * @see com.conradroche.dtd.parser.EntityDeclReader#readEntityDecl(com.conradroche.matra.data.DTDData)
	 */
	public Entity readEntityDecl(DTDData data) throws DTDSyntaxException {
		
		if(!isEntityDeclStart(data)) {
			return null; 
		}
		//[70]    EntityDecl    ::=    GEDecl | PEDecl 
		//[71]    GEDecl    ::=    '<!ENTITY' S Name S EntityDef S? '>' 
		//[72]    PEDecl    ::=    '<!ENTITY' S '%' S Name S PEDef S? '>' 

		data.getNextToken(); // '<!ENTITY'
		data.skipWhiteSpace(); // S
		
		if(data.lookNextChar() == '%') {
			data.getNextChar(); // '%'
			data.skipWhiteSpace();
			return readParameterEntityDecl(data);
		} else {
			return readGeneralEntityDecl(data);
		}
	}

	/**
	 * CR: JavaDoc: Add javadoc for EntityDeclReaderImpl::readGeneralEntityDecl
	 * 
	 * @param data
	 * 
	 * @return
	 * 
	 * @throws DTDSyntaxException 
	 */
	private Entity readGeneralEntityDecl(DTDData data) throws DTDSyntaxException {

		//[71] GEDecl      ::= '<!ENTITY' S Name S EntityDef S? '>' 
		//[73] EntityDef   ::= EntityValue | (ExternalID NDataDecl?) 
		//[9]  EntityValue ::= '"' ([^%&"] | PEReference | Reference)* '"' |  "'" ([^%&'] | PEReference | Reference)* "'" 
		//[75] ExternalID  ::= 'SYSTEM' S SystemLiteral | 'PUBLIC' S PubidLiteral S SystemLiteral 
		//[76] NDataDecl   ::= S 'NDATA' S Name 

		//CR: TODO: Instantiate an implementation of GeneralEntity
		GeneralEntity ge = null;
		
		//CR: TODO: better to get next token and check if its a name
		String name = data.getNextName(); // Name
		ge.setEntityName(name);
		
		data.skipWhiteSpace(); // S
		
		char quot = data.lookNextChar();
		
		if(isQuotChar(quot)) {
			readEntityValue(ge, data);
		} else {
			readExternalID(ge, data);
			//read - NDataDecl?
			data.skipWhiteSpace();
			if(data.nextTokenEquals(NDATA)) {
				data.getNextToken(); // 'NDATA'
				data.skipWhiteSpace();
				String notationName = data.getNextName();
				ge.setNotationName(notationName);
			}
		}
		
		readDeclEnd(data);
		
		return ge;
	}
	
	private void readDeclEnd(DTDData data) throws DTDSyntaxException {
		data.skipWhiteSpace(); // S?
		
		if(data.lookNextChar() != '>') {
			throw new DTDSyntaxException("Invalid Entity declaration encountered at location " + 
						data.getCurrentLocation() + ". Was expecting '>', got char '" + data.lookNextChar() + "'."); 
		}

		data.getNextChar(); // '>'
	}

	/**
	 * Checks if the char specified is 
	 * a quotation mark - either single (')
	 * or double (").
	 * 
	 * @param c The char to check.
	 * 
	 * @return <code>true</code> is the 
	 * 		specified char is a quote
	 * 		char; <code>false</code>
	 * 		otherwise.
	 */
	private boolean isQuotChar(char c) {
		
		if(c == '\'' || c == '\"') {
			return true;
		}
		
		return false;
	}
	
	/**
	 * CR: JavaDoc: Add javadoc for EntityDeclReaderImpl::readExternalID
	 * 
	 * @param ent   
	 * @param data
	 * 
	 * @throws DTDSyntaxException 
	 */
	private void readExternalID(Entity ent, DTDData data) throws DTDSyntaxException {

		//[75] ExternalID    ::= 'SYSTEM' S SystemLiteral | 'PUBLIC' S PubidLiteral S SystemLiteral 
		//[11] SystemLiteral ::= ('"' [^"]* '"') | ("'" [^']* "'")  
		//[12] PubidLiteral  ::= '"' PubidChar* '"' | "'" (PubidChar - "'")* "'" 
		//[13] PubidChar     ::= #x20 | #xD | #xA | [a-zA-Z0-9] | [-'()+,./:=?;!*#@$_%] 

		String token = data.getNextToken();
		
		if(token.equals(EXT_ID_SYSTEM)) {
			
			data.skipWhiteSpace(); // S
			
			char quot = data.lookNextChar();
		
			if(!isQuotChar(quot)) {
				throw new DTDSyntaxException("A system literal should be enclosed in quotes (\' or \"); found invalid char - '"
							+ quot + "'.");
			}
			
			String sysLiteral = data.getNextToken(quot); // SystemLiteral
			ent.setSystemIdentifier(sysLiteral);
			
		} else if(token.equals(EXT_ID_PUBLIC)) {
			
			data.skipWhiteSpace(); // S
			
			char quot = data.lookNextChar();
		
			if(!isQuotChar(quot)) {
				throw new DTDSyntaxException("A public literal should be enclosed in quotes (\' or \"); found invalid char - '"
							+ quot + "'.");
			}

			//CR: TODO: check for valid public literal chars too.			
			String pubidLiteral = data.getNextToken(quot); // PubidLiteral
			ent.setPublicIdentifier(pubidLiteral);
			
			quot = data.lookNextChar();
		
			if(!isQuotChar(quot)) {
				throw new DTDSyntaxException("A system literal should be enclosed in quotes (\' or \"); found invalid char - '"
							+ quot + "'.");
			}
			
			String sysLiteral = data.getNextToken(quot); // SystemLiteral
			ent.setSystemIdentifier(sysLiteral);
			
		} else {
			throw new DTDSyntaxException("Got Invalid token while reading an ExternalID - '" + token +
						"' was expecting either 'SYSTEM' or 'PUBLIC'.");
		}
	}

	/**
	 * CR: JavaDoc: Add javadoc for EntityDeclReaderImpl::readEntityValue
	 * 
	 * @param ent
	 * @param data
	 * 
	 * @throws DTDSyntaxException 
	 */
	private void readEntityValue(Entity ent, DTDData data) throws DTDSyntaxException {

		//[9]  EntityValue ::= '"' ([^%&"] | PEReference | Reference)* '"' |  "'" ([^%&'] | PEReference | Reference)* "'" 
		
		char quot = data.lookNextChar();
		
		if(quot != '\'' && quot != '\"') {
			throw new DTDSyntaxException(
				"The entity value for the entity should be enclosed in quotes (\' or \"." +
				"Encountered char '" + quot + "' instead.");
		}
		
		//CR: TODO: DTDData::getNextToken should throw an exception if EOF encountered before delim.
		String entityValue = data.getNextToken(quot);
		
		ent.setLiteralValue(entityValue);
		
		//CR: TODO: Do more checks on the attribute default value - Maybe not in this class.
		// i) There should be no '<' char in this value.
		// ii) If an & is encountered, it should follow the Reference production.
		
		//[67]    Reference    ::=    EntityRef | CharRef 
		//[68]    EntityRef    ::=    '&' Name ';'  
		//[69]    PEReference    ::=    '%' Name '; 
	}

	/**
	 * CR: JavaDoc: Add javadoc for EntityDeclReaderImpl::readParameterEntityDecl
	 * 
	 * @param data
	 * 
	 * @return
	 * 
	 * @throws DTDSyntaxException 
	 */
	private Entity readParameterEntityDecl(DTDData data) throws DTDSyntaxException {

		//[72] PEDecl      ::= '<!ENTITY' S '%' S Name S PEDef S? '>' 
		//[74] PEDef       ::= EntityValue | ExternalID 
		//[9]  EntityValue ::= '"' ([^%&"] | PEReference | Reference)* '"' |  "'" ([^%&'] | PEReference | Reference)* "'" 
		//[75] ExternalID  ::= 'SYSTEM' S SystemLiteral | 'PUBLIC' S PubidLiteral S SystemLiteral
		 
		//CR: TODO: Instantiate an implementation of ParameterEntity
		ParameterEntity pe = null;
		
		//CR: TODO: better to get next token and check if its a name
		String name = data.getNextName(); // Name
		pe.setEntityName(name);
		
		data.skipWhiteSpace(); // S
		
		char quot = data.lookNextChar();
		if(quot == '\'' || quot == '\"') {
			readEntityValue(pe, data);
		} else {
			readExternalID(pe, data); 
		}
		
		readDeclEnd(data);
		
		return pe;
	}

	/**
	 * Checks if there is an entity declaration
	 * at the current location of the data.
	 * 
	 * @param data The data to be parsed.
	 * 
	 * @return <code>true</code> if the current location
	 * 	of the data has an entity declaration; 
	 * 	<code>false</code> otherwise.
	 * 
	 * @see com.conradroche.dtd.parser.EntityDeclReader#isEntityDeclStart(com.conradroche.matra.data.DTDData)
	 */
	public boolean isEntityDeclStart(DTDData data) {

		if(data == null || data.endOfData()) {
			return false;
		}
		
		return data.nextTokenEquals(ENTITY_DECL_START);
	}
}
